﻿using System;
using System.Diagnostics;

using Storm.TextEditor.Editor.Text;
using Storm.TextEditor.Parser.Objects;

namespace Storm.TextEditor.Editor.Interacting
{
    /// <summary>
    /// Represents the caret that indicates the user's current position in a document.
    /// </summary>
    public sealed class Caret
    {
        #region Members

        private bool blink = false;
        private int oldLogicalXPos = 0;

        private TextPoint position = null;
        private TextEditorBase control = null;

        #region Events

        /// <summary>
        /// Occurs when the caret's position has changed.
        /// </summary>
        public event EventHandler Change = null;

        #endregion

        #endregion

        #region Properties

        /// <summary>
        /// Gets the old logical x position.
        /// </summary>
        public int OldLogicalXPos
        {
            get { return oldLogicalXPos; }
        }

        /// <summary>
        /// Gets or sets the position of the caret.
        /// </summary>
        public TextPoint Position
        {
            get { return position; }
            set
            {
                position = value;
                position.Change += new EventHandler(PositionChange);
                OnChange();
            }
        }

        /// <summary>
        /// Gets or sets whether the caret should blink.
        /// </summary>
        public bool Blink
        {
            get { return blink; }
            set { blink = value; }
        }

        /// <summary>
        /// Gets the word that the caret is placed on.
        /// This only applies if the active row is fully parsed.
        /// </summary>
        public Word CurrentWord
        { get { return control.Document.GetWordFromPos(Position); } }

        /// <summary>
        /// Gets the row that the caret is placed on.
        /// </summary>
        public Row CurrentRow
        { get { return control.Document[Position.Y]; } }

        /// <summary>
        /// Gets the current offset of the caret.
        /// </summary>
        public int Offset
        {
            get
            {
                if (Position.Y >= control.Document.Lines.Length)
                    return 0;
                TextRange tr = new TextRange(0, 0, Position.X, Position.Y);
                int l = control.Document.GetRange(tr).Length;
                int tl = control.Document.Text.Length;
                return Math.Min(tl, l);
            }
        }

        /// <summary>
        /// Gets or sets the logical position of the caret.
        /// </summary>
        public TextPoint LogicalPosition
        {
            get
            {
                if (Position.X < 0)
                    return new TextPoint(0, Position.Y);

                Row xtr = CurrentRow;
                int x = 0;
                if (xtr == null)
                    return new TextPoint(0, 0);

                int max = xtr.Text.Length;
                int Padd = Math.Max(Position.X - xtr.Text.Length, 0);
                string PaddStr = new String(' ', Padd);
                string TotStr = xtr.Text + PaddStr;

                foreach (char c in TotStr.ToCharArray(0, Position.X))
                {
                    if (c == '\t')
                        x += control.TabSpaces - (x % control.TabSpaces);
                    else
                        x++;
                }

                return new TextPoint(x, Position.Y);
            }
            set
            {
                Row xtr = CurrentRow;
                int x = 0;
                int xx = 0;

                if (value.X > 0)
                {
                    char[] chars = xtr.Text.ToCharArray();
                    int i = 0;

                    while (x < value.X)
                    {
                        char c;
                        if (i < chars.Length)
                            c = chars[i];
                        else
                            c = ' ';

                        xx++;
                        if (c == '\t')
                            x += control.TabSpaces - (x % control.TabSpaces);
                        else
                            x++;

                        i++;
                    }
                }

                Position.Y = value.Y;
                Position.X = xx;
            }
        }

        #endregion

        #region Methods

        #region Public

        /// <summary>
        /// Moves the caret right one step.
        /// </summary>
        public void MoveRight(bool select)
        {
            this.CropPosition();
            this.Position.X++;

            if (this.CurrentRow.IsCollapsed == true)
            {
                if (this.Position.X > this.CurrentRow.ExpansionEndChar)
                {
                    this.Position.Y = this.CurrentRow.ExpansionEndRow.Index;
                    this.Position.X = this.CurrentRow.ExpansionEndRow.ExpansionStartChar;
                    this.CropPosition();
                }

                this.RememberXPos();
                this.CaretMoved(select);
            }
            else
            {
                Row row = this.CurrentRow;
                if (this.Position.X > row.Text.Length && control.VirtualWhitespace == false)
                {
                    if (this.Position.Y < control.Document.Count - 1)
                    {
                        this.MoveDown(select);
                        this.Position.X = 0;
                        this.CropPosition();
                    }
                    else
                        this.CropPosition();
                }

                this.RememberXPos();
                this.CaretMoved(select);
            }
        }

        /// <summary>
        /// Moves the caret up one row.
        /// </summary>
        public void MoveUp(bool select)
        {
            this.CropPosition();
            int x = oldLogicalXPos;

            if (this.CurrentRow != null && this.CurrentRow.PreviousVisibleRow != null)
            {
                this.Position.Y = this.CurrentRow.PreviousVisibleRow.Index;
                if (this.CurrentRow.IsCollapsed == true)
                    x = 0;
            }

            this.CropPosition();
            this.LogicalPosition = new TextPoint(x, this.Position.Y);
            this.CropPosition();
            this.CaretMoved(select);
        }

        /// <summary>
        /// Moves the caret up x rows.
        /// </summary>
        /// <param name="rows">Number of rows the caret should be moved up.</param>
        /// <param name="select">Whether to select the character between the positions the caret has from and to.</param>
        public void MoveUp(int rows, bool select)
        {
            this.CropPosition();
            int x = oldLogicalXPos;
            int pos = this.CurrentRow.VisibleIndex;

            pos -= rows;
            if (pos < 0)
                pos = 0;

            Row r = control.Document.VisibleRows[pos];
            pos = r.Index;

            this.Position.Y = pos;
            if (this.CurrentRow.IsCollapsed == true)
                x = 0;

            this.CropPosition();
            this.LogicalPosition = new TextPoint(x, this.Position.Y);
            this.CropPosition();
            this.CaretMoved(select);
        }

        /// <summary>
        /// Moves the caret down x rows.
        /// </summary>
        /// <param name="rows">The number of rows the caret should be moved down.</param>
        /// <param name="select">Whether to select the character between the positions the caret has from and to.</param>
        public void MoveDown(int rows, bool select)
        {
            int x = oldLogicalXPos;
            this.CropPosition();

            try
            {
                int pos = this.CurrentRow.VisibleIndex;
                pos += rows;
                if (pos > control.Document.VisibleRows.Count - 1)
                    pos = control.Document.VisibleRows.Count - 1;

                Row row = control.Document.VisibleRows[pos];
                pos = row.Index;
                this.Position.Y = pos;
                if (this.CurrentRow.IsCollapsed == true)
                    x = 0;
            }
            finally
            {
                this.CropPosition();
                this.LogicalPosition = new TextPoint(x, this.Position.Y);
                this.CropPosition();
                this.CaretMoved(select);
            }
        }

        /// <summary>
        /// Moves the caret down one row.
        /// </summary>
        public void MoveDown(bool select)
        {
            this.CropPosition();
            int x = oldLogicalXPos;

            Row row = this.CurrentRow;
            Row downRow = row.NextVisibleRow;
            if (downRow != null)
            {
                this.Position.Y = downRow.Index;
                if (this.CurrentRow.IsCollapsed == true)
                    x = 0;
            }

            this.CropPosition();
            this.LogicalPosition = new TextPoint(x, this.Position.Y);
            this.CropPosition();
            this.CaretMoved(select);
        }

        /// <summary>
        /// Moves the caret left one step.
        /// </summary>
        public void MoveLeft(bool select)
        {
            this.CropPosition();
            this.Position.X--;

            if (this.CurrentRow.IsCollapsedEndPart == true)
            {
                if (this.Position.X < this.CurrentRow.ExpansionStartChar)
                {
                    if (this.CurrentRow.ExpansionStartRow.Index == -1)
                        Debugger.Break();

                    this.Position.Y = this.CurrentRow.ExpansionStartRow.Index;
                    this.Position.X = this.CurrentRow.ExpansionStartRow.ExpansionEndChar;
                    this.CropPosition();
                }

                this.RememberXPos();
                this.CaretMoved(select);
            }
            else
            {
                if (this.Position.X < 0)
                {
                    if (this.Position.Y > 0)
                    {
                        this.MoveUp(select);
                        this.CropPosition();

                        Row row = this.CurrentRow;
                        this.Position.X = row.Text.Length;
                        if (CurrentRow.IsCollapsed)
                        {
                            this.Position.Y = CurrentRow.ExpansionEndRow.Index;
                            this.Position.X = CurrentRow.Text.Length;
                        }
                    }
                    else
                        this.CropPosition();
                }

                this.RememberXPos();
                this.CaretMoved(select);
            }
        }

        /// <summary>
        /// Moves the caret to the first non whitespace column at the active row.
        /// </summary>
        public void MoveHome(bool select)
        {
            this.CropPosition();
            if (this.CurrentRow.IsCollapsedEndPart == true)
            {
                this.Position.Y = CurrentRow.ExpansionStartRow.Index;
                this.MoveHome(select);
            }
            else
            {
                int i = this.CurrentRow.GetLeadingWhitespace().Length;
                if (this.Position.X == i)
                    this.Position.X = 0;
                else
                    this.Position.X = i;

                this.RememberXPos();
                this.CaretMoved(select);
            }
        }

        /// <summary>
        /// Moves the caret to the end of a row, ignoring any whitespace characters at the end of the row.
        /// </summary>
        public void MoveEnd(bool select)
        {
            if (this.CurrentRow.IsCollapsed == true)
            {
                this.Position.Y = CurrentRow.ExpansionEndRow.Index;
                this.MoveEnd(select);
            }
            else
            {
                this.CropPosition();
                Row row = this.CurrentRow;
                this.Position.X = row.Text.Length;

                this.RememberXPos();
                this.CaretMoved(select);
            }
        }

        /// <summary>
        /// Updates the position of the caret.
        /// </summary>
        public void CaretMoved(bool select)
        {
            control.ScrollIntoView();
            if (select == false)
                control.Selection.ClearSelection();
            else
                control.Selection.MakeSelection();
        }

        /// <summary>
        /// Moves the caret to the first column of the active row.
        /// </summary>
        public void MoveAbsoluteHome(bool select)
        {
            this.Position.X = 0;
            this.Position.Y = 0;

            this.RememberXPos();
            this.CaretMoved(select);
        }

        /// <summary>
        /// Moves the caret to the absolute end of the active row.
        /// </summary>
        public void MoveAbsoluteEnd(bool select)
        {
            this.Position.X = control.Document[control.Document.Count - 1].Text.Length;
            this.Position.Y = control.Document.Count - 1;

            this.RememberXPos();
            this.CaretMoved(select);
        }

        /// <summary>
        /// Returns the segment that the caret currently is in.
        /// </summary>
        /// <returns>The segment that the caret currently is in.</returns>
        public Segment CurrentSegment()
        {
            return control.Document.GetSegmentFromPos(this.Position);
        }

        /// <summary>
        /// Sets the position of the caret.
        /// </summary>
        public void SetPosition(TextPoint pos)
        {
            this.Position = pos;
            this.RememberXPos();
        }

        /// <summary>
        /// Confines the caret to a valid position within the active document.
        /// </summary>
        public void CropPosition()
        {
            if (this.Position.X < 0)
                this.Position.X = 0;

            if (this.Position.Y >= control.Document.Count)
                this.Position.Y = control.Document.Count - 1;

            if (this.Position.Y < 0)
                this.Position.Y = 0;

            Row row = this.CurrentRow;
            if (this.Position.X > row.Text.Length && control.VirtualWhitespace == false)
                this.Position.X = row.Text.Length;
        }

        #endregion

        #region Private

        /// <summary>
        /// 'Remembers' the current x coordinate position by storing it in a variable.
        /// </summary>
        private void RememberXPos()
        {
            oldLogicalXPos = this.LogicalPosition.X;
        }

        #endregion

        #region EventHandlers

        private void PositionChange(object s, EventArgs e)
        {
            this.OnChange();
        }

        /// <summary>
        /// Raises the OnChange event.
        /// </summary>
        private void OnChange()
        {
            if (this.Change != null)
                this.Change(this, null);
        }

        #endregion

        #endregion

        /// <summary>
        /// Initializes a new instance of Caret.
        /// </summary>
        public Caret(TextEditorBase control)
        {
            this.Position = new TextPoint(0, 0);
            this.control = control;
        }
    }
}
